﻿using System;
using System.Diagnostics;
using RaytracerLib.MathLib;
using RaytracerLib.RenderableObjects;
using System.Collections;
using System.Collections.Generic;
using TomanuExtensions;
using RaytracerLib.Rays;
using RaytracerLib.Attenuations;
using RaytracerLib.Lights;
using NLog;
using RaytracerLib.Materials;
using System.Globalization;

namespace RaytracerLib
{
    public class Intersection
    {
        public Intersection PrevIntersection;
        public Scene Scene;
        public Vector3 Pos;
        public double Dist = Double.PositiveInfinity;
        public bool BackHit;
        public Intersection ReflectedIntersection;
        public Intersection RefractedIntersection;
        public Ray SourceRay;
        public Material Material;

        /// <summary>
        /// Valid only for triangle intersection.
        /// </summary>
        public Vector2 BaricentricUV;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_reflected_object_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private RenderableObject m_reflected_object;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_refracted_object_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private RenderableObject m_refracted_object;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Ray m_reflected_ray;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Ray m_refracted_ray;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_refracted_ray_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Vector3 m_normal;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_normal_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Vector3 m_shade_normal;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_shade_normal_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private ColorFloat m_color;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_color_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Vector2 m_uv = Vector2.UNDEFINED;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_uv_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Vector3 m_uvw;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_uvw_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Vector3 m_local_pos;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_local_pos_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private double m_refraction_indice_n1;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_refraction_indice_n1_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_refraction_indice_n2_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private double m_refraction_indice_n2;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private bool m_inner_attenuation_b;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private Attenuation m_inner_attenuation;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private RenderableObject m_scene_object;

        public RenderableObject SceneObject
        {
            get
            {
                return m_scene_object;
            }
            set
            {
                m_scene_object = value;
                if (Material == null)
                    Material = m_scene_object.DefaultMaterial;
            }
        }

        #if DEBUG
        public long ID;
        public static long IDCounter = 0;
        #endif

        public Intersection()
        {
            #if DEBUG
            IDCounter++;
            ID = IDCounter;
            #endif
        }

        public bool Exists
        {
            get
            {
                return Dist != Double.PositiveInfinity;
            }
        }

        public Vector3 Normal
        {
            get
            {
                Debug.Assert(Exists);

                if (!m_normal_b)
                {
                    m_normal = SceneObject.GetNormal(this);
                    m_normal_b = true;
                }

                return m_normal;
            }
        }

        public Vector3 ShadeNormal
        {
            get
            {
                Debug.Assert(Exists);

                if (!m_shade_normal_b)
                {
                    m_shade_normal = SceneObject.GetShadeNormal(this);

                    if (Material.HasTangents)
                    {
                        Tangents surface_tangents = Material.Tangents.GetTangents(this);

                        Vector3 tx, ty;
                        SceneObject.GetTangents(this, out tx, out ty);

                        Debug.Assert(tx.IsNormalized);
                        Debug.Assert(ty.IsNormalized);

                        Vector3 ry = Vector3.CrossProduct(tx, m_shade_normal);
                        Vector3 rx = Vector3.CrossProduct(m_shade_normal, ty);

                        Matrix4 mx = Matrix4.CreateRotateAroundVector(rx, surface_tangents.Y.Y, surface_tangents.Y.X);
                        Matrix4 my = Matrix4.CreateRotateAroundVector(ry, surface_tangents.X.Y, surface_tangents.X.X);

                        m_shade_normal = mx * my * m_shade_normal;
                    }

                    m_shade_normal_b = true;
                }

                return m_shade_normal;
            }
        }

        public double RefractionIndiceN1
        {
            get
            {
                if (!m_refraction_indice_n1_b)
                {
                    if (ReflectedSideObject != null)
                        m_refraction_indice_n1 = ReflectedSideObject.DefaultMaterial.RefractionIndice;
                    else
                        m_refraction_indice_n1 = Scene.RenderOptions.EnvironmentRefractionIndice;

                    m_refraction_indice_n1_b = true;
                }

                return m_refraction_indice_n1;
            }
        }

        public double RefractionIndiceN2
        {
            get
            {
                if (!m_refraction_indice_n2_b)
                {
                    if (RefractedSideObject != null)
                        m_refraction_indice_n2 = RefractedSideObject.DefaultMaterial.RefractionIndice;
                    else
                        m_refraction_indice_n2 = Scene.RenderOptions.EnvironmentRefractionIndice;

                    m_refraction_indice_n2_b = true;
                }

                return m_refraction_indice_n2;
            }
        }

        /// <summary>
        /// Object for refraction indice N1, source ray, reflected ray.
        /// </summary>
        public RenderableObject ReflectedSideObject
        {
            get
            {
                if (!m_reflected_object_b)
                {
                    if (BackHit && SceneObject.Closed)
                        m_reflected_object = SceneObject;
                    else
                    {
                        if (PrevIntersection != null)
                        {
                            if (SourceRay.RaySurfaceSide == RaySurfaceSide.ReflectedSide)
                                m_reflected_object = PrevIntersection.ReflectedSideObject;
                            else
                            {
                                Debug.Assert(SourceRay.RaySurfaceSide == RaySurfaceSide.RefractedSide);
                                m_reflected_object = PrevIntersection.RefractedSideObject;
                            }
                        }
                        else
                        {
                            m_reflected_object = Scene.SpatialSubdivisioner.FindOuterObject(this);
                        }
                    }

                    m_reflected_object_b = true;
                }

                return m_reflected_object;
            }
        }

        /// <summary>
        /// Object for N2, refracted ray.
        /// </summary>
        public RenderableObject RefractedSideObject
        {
            get
            {
                if (!m_refracted_object_b)
                {
                    if (!SceneObject.Closed)
                    {
                        if (PrevIntersection != null)
                        {
                            if (PrevIntersection.SourceRay.RaySurfaceSide == RaySurfaceSide.ReflectedSide)
                                m_refracted_object = PrevIntersection.ReflectedSideObject;
                            else if (PrevIntersection.SourceRay.RaySurfaceSide == RaySurfaceSide.RefractedSide)
                                m_refracted_object = PrevIntersection.RefractedSideObject;
                            else
                            {
                                if (PrevIntersection.SourceRay.Dir * PrevIntersection.ShadeNormal < 0)
                                    m_refracted_object = PrevIntersection.ReflectedSideObject;
                                else
                                    m_refracted_object = PrevIntersection.RefractedSideObject;
                            }
                        }
                        else
                            m_refracted_object = Scene.SpatialSubdivisioner.FindOuterObject(this);
                    }
                    else if (BackHit)
                        m_refracted_object = Scene.SpatialSubdivisioner.FindOuterObject(this);
                    else
                        m_refracted_object = SceneObject;

                    m_refracted_object_b = true;
                }

                return m_refracted_object;
            }
        }

        public Ray RefractedRay
        {
            get
            {
                if (!m_refracted_ray_b)
                {
                    Debug.Assert(Exists);
                    Debug.Assert(Normal * SourceRay.Dir < 0);

                    if (Material.HasRefraction)
                    {
                        Scene.Statistics.RefractionCalculations++;
                        bool total_internal_reflection = false;
                        Vector3 refracted_dir = RayMath.Refract(SourceRay.Dir, ShadeNormal, RefractionIndiceN1, 
                            RefractionIndiceN2, ref total_internal_reflection);

                        if (!total_internal_reflection)
                            m_refracted_ray = new RefractedRay(this, refracted_dir);
                        else
                            Scene.Statistics.RayTotalInternalReflections++;
                    }

                    m_refracted_ray_b = true;
                }

                return m_refracted_ray;
            }
        }

        public Ray ReflectedRay
        {
            get
            {
                if (m_reflected_ray == null)
                {
                    Debug.Assert(Exists);
                    //Debug.Assert(Normal * SourceRay.Dir < 0);

                    Scene.Statistics.ReflectionCalculations++;

                    m_reflected_ray = new ReflectedRay(this, 
                        RayMath.Reflect(SourceRay.Dir, ShadeNormal));
                }

                return m_reflected_ray;
            }
        }

        public ColorFloat Color
        {
            get
            {
                if (!m_color_b)
                {
                    if (!Exists)
                        m_color = Scene.RenderOptions.AmbientColor;
                    else
                    {
                        #if DEBUG
                        if (ID == Scene.RenderOptions.DebugBreakIntersectionID)
                            Debugger.Break();
                        #endif

                        Scene.Statistics.ColorsCalculated++;

                        m_color = CalculateLight();

                        if (Material.HasRefraction && (RefractedRay != null))
                        {
                            ColorFloat refraction_factor =
                                Material.RefractionTransmittance.GetColor(this);

                            Scene.Statistics.FresnelCalculations++;
                            double fresnel_refraction_factor = 1 - FresnellMath.FresnelReflectionFactor(
                                RefractedRay.Dir, -ShadeNormal, RefractionIndiceN2, RefractionIndiceN1);

                            refraction_factor = refraction_factor * fresnel_refraction_factor;

                            RefractedIntersection = Scene.SpatialSubdivisioner.HitTest(this, RefractedRay);
                            m_color += RefractedIntersection.Color * refraction_factor;
                        }

                        if (Material.HasReflection)
                        {
                            ColorFloat reflection_factor =
                                Material.ReflectionTransmittance.GetColor(this);

                            if (Material.HasRefraction)
                            {
                                Scene.Statistics.FresnelCalculations++;
                                double fresnel_reflection_factor = FresnellMath.FresnelReflectionFactor(
                                    ReflectedRay, RefractedRay, ShadeNormal, RefractionIndiceN1, RefractionIndiceN2);
                                reflection_factor = reflection_factor * fresnel_reflection_factor;
                            }

                            if (reflection_factor != ColorFloat.Black)
                            {
                                ReflectedIntersection = Scene.SpatialSubdivisioner.HitTest(this, ReflectedRay);
                                m_color += ReflectedIntersection.Color * reflection_factor;
                            }
                        }

                        if (InnerAttenuation != null)
                            m_color = InnerAttenuation.Attenuate(m_color, Dist);
                    }

                    m_color_b = true;
                }

                return m_color;
            }
        }

        #if DEBUG
        public string DebugLogTree()
        {
            string result = String.Format("\nType: {0}, object: {4}, backhit: {1}, ID: {2}, Pos: {3}", 
                SourceRay.GetType().Name, BackHit, ID, Pos, SceneObject);
            if ((ReflectedIntersection != Scene.NoIntersection) && (ReflectedIntersection != null))
                result += ReflectedIntersection.DebugLogTree().Replace("\n", "\n    ");
            if ((RefractedIntersection != Scene.NoIntersection) && (RefractedIntersection != null))
                result += RefractedIntersection.DebugLogTree().Replace("\n", "\n    ");
            return result;
        }
        #endif

        #if DEBUG
        public string DebugPath()
        {
            return ((PrevIntersection != null) ? PrevIntersection.DebugPath() : "") + "\\" + 
                ID.ToString();
        }
        #endif

        #if DEBUG
        public List<Light> GetLights()
        {
            List<Light> lights = new List<Light>();
            CalculateLight(lights);
            return lights;
        }
        #endif

        public Attenuation InnerAttenuation 
        {
            get
            {
                if (!m_inner_attenuation_b)
                {
                    if (ReflectedSideObject != null)
                        m_inner_attenuation = ReflectedSideObject.DefaultMaterial.Attenuation;
                    else
                        m_inner_attenuation = Scene.RenderOptions.EnvironmentAttenuation;

                    m_inner_attenuation_b = true;
                }

                return m_inner_attenuation;
            }
        }

        private ColorFloat CalculateLight(List<Light> a_lights = null)
        {
            ColorFloat diffuse_light = Material.HasAmbient ?
                Scene.RenderOptions.AmbientColor *
                    Material.Ambient.GetColor(this) : ColorFloat.Black;

            ColorFloat specular_light = ColorFloat.Black;

            ColorFloat diffuse_coefficient = Material.HasDiffuse ? 
                Material.Diffuse.GetColor(this) : ColorFloat.Black;

            ColorFloat specular_highlight_coefficient =
                Material.HasSpecularHighlight ?
                Material.SpecularHighlight.GetColor(this) : ColorFloat.Black;

            double specular_exponent = Material.HasSpecularHighlight ?
                Material.SpecularHighlightExponent.GetValue(this) : 0;

            bool calculate_diffuse = diffuse_coefficient != ColorFloat.Black;
            bool calculate_specular = specular_highlight_coefficient != ColorFloat.Black;

            bool has_refraction = Material.HasRefraction;

            foreach (Light light in Scene.Lights)
            {
                Vector3 dir_to_light_real = light.Pos - Pos;
                Vector3 dir_to_light_real_norm = dir_to_light_real.Normalized;
                Vector3 dir_to_light_imag_norm = dir_to_light_real_norm;

                double fresnel_factor = 1;
                RaySurfaceSide ray_surface_side = RaySurfaceSide.ReflectedSide;

                if (has_refraction)
                {
                    if (dir_to_light_real_norm * ShadeNormal < 0)
                    {
                        Scene.Statistics.ReflectionCalculations++;
                        dir_to_light_imag_norm = RayMath.Mirror(dir_to_light_real_norm, ShadeNormal);
                        ray_surface_side = RaySurfaceSide.RefractedSide;
                    }
                    else
                    {
                        Scene.Statistics.FresnelCalculations++;
                        fresnel_factor = FresnellMath.FresnelReflectionFactor(dir_to_light_real_norm, ShadeNormal,
                            RefractionIndiceN1, RefractionIndiceN2);
                    }
                }

                if (!has_refraction)
                {
                    // Deal with situation when ShadowNormal can catch light through
                    // surface.
                    if ((dir_to_light_imag_norm * Normal) < 0)
                        continue;

                    if (dir_to_light_real_norm * ShadeNormal < 0)
                        continue;
                }

                // Case when ShadowNormal is parallel to surface.
                if ((ShadeNormal * Normal).IsAlmostRelativeEquals(0))
                    continue;

                ShadowRay ray_to_light = new ShadowRay(this, dir_to_light_real_norm, ray_surface_side);
                ColorFloat light_color = Scene.ShadowTester.ShadowTest(this, ray_to_light, light);

                if (light_color == ColorFloat.Black)
                    continue;

                #if DEBUG
                if (a_lights != null)
                    a_lights.Add(light);
                #endif

                if (calculate_diffuse)
                {
                    // Lambertian reflectance
                    double dir_to_light_shade_normal_angle = dir_to_light_imag_norm * ShadeNormal;
                    diffuse_light += light_color * dir_to_light_shade_normal_angle * fresnel_factor;
                }

                if (calculate_specular)
                {
                    // Phong reflection
                    double specular_angle = ReflectedRay.Dir * dir_to_light_imag_norm;
                    if (specular_angle > 0)
                    {
                        specular_light += (light_color * Math.Pow(specular_angle,
                            specular_exponent)) * specular_angle * fresnel_factor;
                    }
                }
            }

            return diffuse_light * diffuse_coefficient + 
                specular_light * specular_highlight_coefficient;
        }

        public override string ToString()
        {
            if (Exists)
            {
                return String.Format("object: {0}; backhit: {1}",
                    SceneObject.Name, BackHit);
            }
            else
                return "<Non exists>";
        }

        public Vector3 UVW
        {
            get
            {
                if (!m_uvw_b)
                {
                    m_uvw = SceneObject.GetUVW(this);

                    if (!(SceneObject is PlaneObject))
                    {
                        Debug.Assert(m_uvw.X.IsAlmostRelativeGreaterOrEqualThen(0));
                        Debug.Assert(m_uvw.X.IsAlmostRelativeLessOrEqualThen(1));
                        Debug.Assert(m_uvw.Y.IsAlmostRelativeGreaterOrEqualThen(0));
                        Debug.Assert(m_uvw.Y.IsAlmostRelativeLessOrEqualThen(1));
                        Debug.Assert(m_uvw.Z.IsAlmostRelativeGreaterOrEqualThen(0));
                        Debug.Assert(m_uvw.Z.IsAlmostRelativeLessOrEqualThen(1));
                    }

                    m_uvw_b = true;
                }

                return m_uvw;
            }
        }

        public Vector3 LocalPos
        {
            get
            {
                if (!m_local_pos_b)
                {
                    m_local_pos = SceneObject.GetLocalPos(this);
                    m_local_pos_b = true;
                }

                return m_local_pos;
            }
            set
            {
                m_local_pos = value;
                m_local_pos_b = true;
            }
        }



        public Vector2 UV
        {
            get
            {
                if (!m_uv_b)
                {
                    m_uv = SceneObject.GetUV(this);

                    if (!(SceneObject is PlaneObject))
                    {
                        Debug.Assert(m_uv.X.IsAlmostRelativeGreaterOrEqualThen(0));
                        Debug.Assert(m_uv.X.IsAlmostRelativeLessOrEqualThen(1));
                        Debug.Assert(m_uv.Y.IsAlmostRelativeGreaterOrEqualThen(0));
                        Debug.Assert(m_uv.Y.IsAlmostRelativeLessOrEqualThen(1));
                    }

                    m_uv_b = true;
                }
                return m_uv;
            }
            set
            {
                m_uv = value;

                if (!(SceneObject is PlaneObject))
                {
                    Debug.Assert(m_uv.X.IsAlmostRelativeGreaterOrEqualThen(0));
                    Debug.Assert(m_uv.X.IsAlmostRelativeLessOrEqualThen(1));
                    Debug.Assert(m_uv.Y.IsAlmostRelativeGreaterOrEqualThen(0));
                    Debug.Assert(m_uv.Y.IsAlmostRelativeLessOrEqualThen(1));
                }

                m_uv_b = true;
            }
        }
    }
}